//! Declaritive world instance parser.

#![allow(unused)]

use std::collections::*;

use thiserror::Error;

use craftdig_core::registrar;
use craftdig_datamodel::{
    block::{self, state::BlockStateSnapshot},
    world,
};
use craftdig_math::*;

#[derive(Debug, Error)]
pub enum Error {
    #[error("invalid block name '{0}'")]
    UnknownBlockName(String),

    #[error("malformed command '{0}'")]
    MalformedCommand(String),

    #[error("invalid command '{0}'")]
    InvalidCmd(String),

    #[error("invalid arg for command {0}: {1}")]
    InvalidArg(&'static str, String),

    #[error("unset block char '{0}'")]
    UnsetBlockChar(char),

    #[error("tried to move beyond boundary from {0:?}")]
    HitBoundary(SubchunkPos),

    #[error("duplicate write for chunk {0:?}")]
    DuplicateChunks(ChunkCoord),
}

impl Error {
    pub fn invalid_arg<S: ToString>(c: &'static str, s: S) -> Self {
        Self::InvalidArg(c, s.to_string())
    }
}

/// Utility to create a world instance from a lazy text format.
pub struct Instantiator {
    ops: HashMap<char, block::BlkId>,
    default_blkid: block::BlkId,
}

impl Instantiator {
    pub fn new(ops: HashMap<char, block::BlkId>, default_blkid: block::BlkId) -> Self {
        Self { ops, default_blkid }
    }

    pub fn init(
        tbl: Vec<(char, String)>,
        reg: &registrar::GameRegistrar,
        default: &str,
    ) -> Result<Self, Error> {
        let mut ops = HashMap::new();
        for (k, v) in tbl {
            let id = reg
                .block_ty_reg()
                .resolve_name(&v)
                .ok_or_else(|| Error::UnknownBlockName(v.clone()))?;
            ops.insert(k, id);
        }

        let default_blkid = reg
            .block_ty_reg()
            .resolve_name(default)
            .ok_or_else(|| Error::UnknownBlockName(default.to_owned()))?;

        Ok(Self { ops, default_blkid })
    }

    fn get_block_as_bss(&self, arg: &str) -> Result<BlockStateSnapshot, Error> {
        if arg.len() != 1 {
            return Err(Error::invalid_arg("filllayer", arg));
        }

        let c = arg.chars().next().unwrap();
        let blkid = self.ops.get(&c).ok_or(Error::UnsetBlockChar(c))?;
        let bss = BlockStateSnapshot::new_simple(*blkid);
        Ok(bss)
    }

    fn parse_xyz_u8_tuple(&self, arg: &str, cmd: &'static str) -> Result<(u8, u8, u8), Error> {
        let abc = arg
            .split_once(' ')
            .map(|(a, bc)| bc.split_once(' ').map(|(b, c)| (a, b, c)))
            .flatten()
            .map(|(a, b, c)| (a.parse::<u8>(), b.parse::<u8>(), c.parse::<u8>()));
        match abc {
            Some((Ok(x), Ok(y), Ok(z))) => Ok((x, y, z)),
            _ => return Err(Error::invalid_arg(cmd, arg)),
        }
    }

    pub fn render(&self, expr: &str) -> Result<Vec<(ChunkCoord, world::map::ChunkState)>, Error> {
        let mut out = Vec::new();

        let mut cur_chunk = ChunkCoord::from(Vec3i::new(0, 0, 0));
        let mut at = SubchunkPos::new(0, 0, 0);

        let mut chunk = world::map::ChunkState::new_full(self.default_blkid);

        for l in expr.lines() {
            // ignore empty lines or lines that start with #
            if l.is_empty() || l.starts_with('#') {
                continue;
            }

            match l.split_once(' ') {
                Some((cmd, arg)) => match cmd {
                    // Position
                    "p" => {
                        let (x, y, z) = self.parse_xyz_u8_tuple(arg, "p")?;
                        at = SubchunkPos::new(x, y, z);
                    }

                    // FILL LAYER
                    "filllayer" => {
                        let bss = self.get_block_as_bss(arg)?;

                        for i in 0..CHUNK_SIZE {
                            for j in 0..CHUNK_SIZE {
                                let p = SubchunkPos::new(i as u8, at.y(), j as u8);
                                chunk.set_block_state(p, &bss);
                            }
                        }
                    }

                    // SET block
                    "set" => {
                        let bss = self.get_block_as_bss(arg)?;
                        chunk.set_block_state(at, &bss);
                    }

                    // SET block, Move X
                    "setmx" => {
                        let bss = self.get_block_as_bss(arg)?;
                        chunk.set_block_state(at, &bss);

                        if at.x() as usize == CHUNK_SIZE - 1 {
                            return Err(Error::HitBoundary(at));
                        }

                        at = SubchunkPos::new(at.x() + 1, at.y(), at.z());
                    }

                    // SET block, Move Y
                    "setmy" => {
                        let bss = self.get_block_as_bss(arg)?;
                        chunk.set_block_state(at, &bss);

                        if at.y() as usize == CHUNK_SIZE - 1 {
                            return Err(Error::HitBoundary(at));
                        }

                        at = SubchunkPos::new(at.y(), at.y() + 1, at.z());
                    }

                    // SET block, Move Z
                    "setmz" => {
                        let bss = self.get_block_as_bss(arg)?;
                        chunk.set_block_state(at, &bss);

                        if at.z() as usize == CHUNK_SIZE - 1 {
                            return Err(Error::HitBoundary(at));
                        }

                        at = SubchunkPos::new(at.x(), at.y(), at.z() + 1);
                    }

                    s => {
                        return Err(Error::InvalidCmd(s.to_owned()));
                    }
                },

                _ => match l {
                    // move UP
                    "up" => {
                        if at.y() as usize == CHUNK_SIZE - 1 {
                            return Err(Error::HitBoundary(at));
                        }

                        at = SubchunkPos::new(at.x(), at.y() + 1, at.z());
                    }

                    // WRITE to output
                    "write" => {
                        out.push((cur_chunk, chunk.clone()));
                        chunk = world::map::ChunkState::new_full(self.default_blkid);
                    }

                    _ => {
                        return Err(Error::InvalidCmd(l.to_owned()));
                    }
                },
            }
        }

        // Discard if there's duplicate writes
        out.sort_by_key(|(k, _)| *k);
        if let Some(dup) = out.windows(2).filter(|arr| arr[0].0 == arr[1].0).next() {
            let cc = dup[0].0;
            return Err(Error::DuplicateChunks(cc));
        }

        Ok(out)
    }
}

pub const TEST_SCRIPT: &str = "
p 0 0 0

filllayer s
up

# do a weird shape like this:
#  @
# @@@@
p 5 1 5
setmx c
setmx c
setmx c
setmx c
p 6 2 5
set g

# and then another block next to it
p 6 1 7
set c

# then draw something silly
p 3 1 10
setmz c
setmz _
setmz c
p 3 2 11
setmx c
setmx c
setmx c

write
";

pub fn make_base_instantiator() -> Instantiator {
    Instantiator::new(
        [
            ('_', block::BlkId::from(0)),
            ('s', block::BlkId::from(1)),
            ('g', block::BlkId::from(2)),
            ('d', block::BlkId::from(3)),
            ('c', block::BlkId::from(4)),
        ]
        .into_iter()
        .collect(),
        block::BlkId::from(0),
    )
}

#[cfg(test)]
mod tests {
    use craftdig_datamodel::block::BlkId;
    use craftdig_math::*;

    use super::{Instantiator, TEST_SCRIPT};

    #[test]
    fn diag_instantiator() {
        let inst = Instantiator::new(
            [
                ('_', BlkId::zero()),
                ('s', BlkId::from(1)),
                ('c', BlkId::from(2)),
                ('g', BlkId::from(3)),
            ]
            .into_iter()
            .collect(),
            BlkId::from(0),
        );

        let out = inst.render(TEST_SCRIPT).expect("inst: render");

        for (ccoord, chunk) in out {
            for y in 0..CHUNK_SIZE {
                let xz_iter = (0..CHUNK_SIZE).flat_map(|x| (0..CHUNK_SIZE).map(move |z| (x, z)));
                let mut linebuf = String::new();
                let mut any = false;
                for (x, z) in xz_iter {
                    let pos = SubchunkPos::new(x as u8, y as u8, z as u8);
                    let id_raw: u16 = chunk.get_block_id(pos).into();

                    linebuf.push_str(&format!("{}", id_raw));
                    any |= id_raw != 0;
                }

                if any {
                    eprintln!("y={y} {linebuf}");
                }
            }
        }
    }
}
